docs(objectql): name the include-relation boundary of a cross-field comparand, and pin it (#15103, fallback B) - #15781
Merged
zhuangjianguo merged 2 commits intoSep 5, 2026
Conversation
…omparand, and pin it (#15103 fallback B) The #15103 gate was measured before any code: the ADR-0071 join chain lives in NativeSQLStrategy, which declines every { $field } comparand (2026-08-12 ruling, #7598), and the driver that compiles the comparand builds no joins — so the chain cannot serve the where compilation without JOIN planning in the driver, an alias contract through executeAggregate, or a second enforcement site for the #5222 rulings. Fallback B: capability unchanged; the query-syntax page names the boundary and a service-analytics pin measures it over a real engine (dimension and filter member through the join; the comparand refused INVALID_FILTER/400 with duty in include; native declined on that pass). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…clude-relation-cross-field-pushdown
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 5, 2026
zhuangjianguo
marked this pull request as ready for review
September 5, 2026 08:03
zhuangjianguo
enabled auto-merge
September 5, 2026 08:03
zhuangjianguo
deleted the
claude/issue-15103-include-relation-cross-field-pushdown
branch
September 5, 2026 08:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #15103
The gate, measured before any code — it does not open; fallback B is delivered
Answer: the ADR-0071 join chain cannot serve the
wherecompilation of a{ $field }comparand without inventing JOIN planning — or an alias contract, or a second enforcement site for the #5222 rulings. Per the ruling on the card (comment 5548479553), this PR lands B: capability unchanged, the boundary named on the query-syntax page, the #14104 ruling record corrected in a comment on that card, and one pin that measures the boundary over a real engine.Where the chain is, and where the comparand goes (files, functions, order of operations)
packages/services/service-analytics/src/dataset-compiler.tscompileDataset— everyincludepath becomes an entry ofcube.joins(alias to table), underMAX_JOIN_HOPS = 3; the allowlist is every alias.packages/services/service-analytics/src/strategies/native-sql-strategy.tsgenerateSql— a per-queryjoinsmap is filled lazily byqualifyAndRegisterJoinfrom dimensions, measures and filter members (compileFilterNodeleaf, thenresolveFieldSql, thenqualifyAndRegisterJoin); then the allowlist check and per-alias RLS; thenFROM base LEFT JOIN …. So on this path the chain already participates in the predicate — for the left-hand MEMBER. Nothing is fixed before the joins are known; the order of operations is not the obstacle.NativeSQLStrategy.canHandledeclines every query whosewhereor read scope carries a$field— maintainer ruling 2026-08-12, Q1 = B ([spec]service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598), quoted verbatim inside that method: the four [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222 rulings are enforced in ONE place, the driver, with no newStrategyContexthooks and no second implementation. The$fieldshape therefore never reaches the chain.ObjectQLStrategy, thenctx.executeAggregate({ groupBy, aggregations, filter, timezone, context }), thenSqlDriver.aggregate(),applyFilters,applyCrossFieldComparison(packages/drivers/driver-sql/src/sql-driver.ts), where the dotted reference is refused. The bridge carries no joins, noinclude, no alias;sql-driver.ts(17,371 lines) contains zero knex join calls — every statement it builds isFROM "TABLE". At the refusal site there is no join chain in scope at all.Why A is not reachable inside the ruling
includelist — which never reaches the driverincluderelation$fieldinNativeSQLStrategythroughqualifyAndRegisterJoinStrategyContextexposes none of thempackages/speccontract change; not "without a new mechanism"executeAggregate, the engine andDriverQueryQ3 — the three-hop bound
Inherited only on the chain that exists:
cube.joinsis built underMAX_JOIN_HOPS = 3at compile time. The driver's comparand compiler has no chain to inherit from, so route (i) would have had to restate it. Moot under B.What this PR changes
content/docs/protocol/objectql/query-syntax.mdx— one paragraph under "Comparing Two Fields": a dataset'sincludedoes not widen the same-table rule, with the mechanism and the pin named.packages/services/service-analytics/src/__tests__/include-relation-cross-field-boundary.test.ts— the measurement, executable. Over one dataset withinclude: ['duty']on a realSqliteWasmDriver: (1) a dimension and aruntimeFiltermember onduty.grace_dayscompile to oneLEFT JOIN "cross_field_duty" "duty" ON "cross_field_task"."duty" = "duty"."id", with the joined column in the WHERE, and return real grouped rows; (2) the offset arm AND the bare arm of a$fieldcomparand namingduty.grace_daysare refusedINVALID_FILTER/ 400 with the dotted-path message, and the native-SQL spy proves that pass was declined (no SQL emitted, so no join was ever built); (3) the same-table spelling answers rows 2 and 3 on the same fixture. Declared deviation from "docs-only": one test file, publishing nothing; it goes red the day A lands silently. The memory half of the asymmetry stays pinned where the evaluator lives (packages/formula/src/matches-filter-field-reference-offset.test.ts); this package does not depend on@objectstack/formula.packages/formulachange (assumption 6 held). No changeset — nothing published changes (skip-changeset).Clause-②: no— no contract increment, soneeds:contract-reviewis not hung; that reading is stated in the report for the PM to overrule.Zone 2 assumptions, measured
SqliteWasmDriver extends SqlDriver(sqlite-wasm-driver.ts:67) — one compiler serves both.includerefusal isDATASET_INVALID/ 400 — a different code from the driver'sINVALID_FILTER— so A there would have changed the refusal for a caller-visible input.getPathinmatches-filter.tswalks dotted paths; the memory path is unchanged.packages/drivers/driver-sql/src/cross-field-conformance-cases.tsplus the two driver suites andcross-field-offset-dataset.test.ts;packages/qa/dogfood/test/authz-conformance.matrix.tsis untouched.Verification — head
50cf9e8f1(after mergingorigin/mainat7dafaaedd)pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2 src/__tests__/include-relation-cross-field-boundary.test.ts src/__tests__/cross-field-offset-dataset.test.ts— exit 0: "Test Files 2 passed (2) · Tests 82 passed (82)".pnpm --filter @objectstack/service-analytics typecheck— exit 0;tsc --noEmit --listFileslists the new test file (1 hit; the sibling test as control, 1 hit).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(72 commands, derived at50cf9e8f1, no stale-tree warning), every exit code captured before any pipe: 70 exit 0, among themcheck:nul-bytes,check:cross-package-test-inputs,check:test-source-alias,check:doc-anchors,check:docs-single-h1,check:type-check-coverage,@objectstack/spec check:docs, and@objectstack/spec check:skill-examples(green after buildingclientandclient-react: "257 prose examples type-check across 3 surface(s)"). NOT MEASURED, both whole-tree prerequisites this diff cannot narrow:check:dual-build-cjs-loads(exit 3, "PREREQUISITE NOT MET — some package has no dist/") andcheck:type-check-debt(exit 3, "--re-measure cannot run: 23 workspace dependencies have no built type entry point"). Neither reads this diff's two files; CI runs both over its full build.FieldReferenceSchema's TSDoc@examplespells the #14104 shape asduty.grace_days— a relation path SQL push-down refuses — two paragraphs above the prose that says so #15778 — theFieldReferenceSchemaTSDoc@examplestill spells the dotted form the prose beside it says is refused.Generated by Claude Code